22  零售超市销售额预测

22.1 引言销售预测的商业价值

零售超市销售预测:

  • 库存管理: 避免缺货或积压
  • 人员安排: 高峰期增加人手
  • 营销策略: 促销时机选择
  • 财务规划: 现金流预测

22.2 本章学习目标

本章以零售超市周销售额预测为例,把前面章节的数据处理与可视化技能串联成一条完整的建模流水线。通过本章学习,你将掌握:

  1. 销售预测类任务的标准流程:数据读取、特征构造、特征筛选、编码、模型训练与评估
  2. 从日期字段构造时间特征(星期、月份、年份)并按业务判断取舍的思路
  3. 用散点图矩阵与相关系数热力图支撑特征筛选的方法
  4. 分类变量(商店编号、月份)的独热编码(pd.get_dummies)
  5. 训练集/测试集划分、特征标准化与R²评估三个建模前置概念的含义

先修内容:第 章节 10 章(Pandas数据框基础)与第 章节 21 章(相关系数热力图的绘制与解读);绘图细节可回看第 章节 19 章。

22.3 本任务需要的前置概念

在照抄平台代码之前,先建立三个直觉概念。它们分别对应平台代码中的几行关键语句,理解之后再动手,输入平台时就不容易”抄错行”。

22.3.1 训练集与测试集

模型的最终价值在于对没见过的数据做预测,因此不能拿模型”学习过”的数据来给它打分。解决办法是把样本切成两份:一份交给模型学习规律,称为训练集;另一份只用来考试,称为测试集。平台代码中 X_train, X_test, y_train, y_test = train_test_split(...) 一行对应这一步,train_size=0.8 表示八成样本用于训练、两成用于考试,random_state=100 固定切分的随机性,保证每次重跑得到同一份考卷。如果省略这一步,模型可以”背下”训练样本,得到虚高的评估分数。

22.3.2 特征标准化

各特征的量纲相差悬殊:温度是几十的量级,CPI上百,失业率只有个位数。量纲不同并不代表数值大的特征更有用,但会让系数之间失去可比性,也让基于距离与梯度的数值计算变得不稳定。标准化的做法是把每一列变换成均值为0、标准差为1的量纲无关尺度。平台代码中 scaler = StandardScaler()X_train = scaler.fit_transform(X_train)X_test = scaler.transform(X_test) 三行对应这一步。特别要注意:测试集只执行 transform 而不再 fit,即测试集必须沿用训练集学到的均值和标准差,否则相当于让考卷迁就考生,评估随之失效——既可能虚高,也可能像本章模型构建一节的常见错误演示那样大幅偏低,总之不再是泛化能力的可信度量。

22.3.3 R²评估

R²(决定系数)回答的问题是:与”无脑地直接用均值来猜”相比,模型把预测误差降低了多少。R²=1表示完美预测,R²=0表示与均值基准一样差,越接近1越好;在测试集上它还可能为负,意味着模型比均值基准更差。平台代码中 r2 = r2_score(y_test, y_pred) 一行对应这一步,它只用测试集的真实值与预测值计算。R²是线性回归最常用的单一数字摘要,但它不区分高估与低估,也反映不出季节性等误差结构,本章末尾的分析结论会再回到这一点。

22.4 数据准备与探索

任务要求:读取平台内置的 01_Walmart_Store_sales.csv,完成一条完整的销售额预测流水线——从 Date 列提取 weekday/month/year 特征并删除日期、星期、年份等非特征列;用散点图矩阵与相关系数热力图考察各特征与 Weekly_Sales 的关系并剔除 Fuel_Price;对 Storemonth 做独热编码;按 8:2 划分训练集与测试集(random_state=100)并做标准化;训练多元线性回归模型;最后打印测试集 R²。

列表 22.1: 平台原始代码
# 注:01_Walmart_Store_sales.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入相关包
import pandas as pd
import numpy as np  # 导入NumPy数值计算库
import seaborn as sns  # 导入Seaborn可视化库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
from sklearn.preprocessing import StandardScaler  # 导入Scikit-learn的StandardScaler模块
from sklearn.model_selection import train_test_split  # 导入Scikit-learn的train_test_split模块
from sklearn.linear_model import LinearRegression  # 导入Scikit-learn的LinearRegression模块
from sklearn.metrics import r2_score  # 导入Scikit-learn的r2_score模块
 
# 读取数据
df = pd.read_csv('01_Walmart_Store_sales.csv', parse_dates = True, infer_datetime_format = True)
 
# 整理数据
df.Date=pd.to_datetime(df.Date)
 
df['weekday'] = df.Date.dt.weekday  # 提取日期时间属性
df['month'] = df.Date.dt.month  # 提取日期时间属性
df['year'] = df.Date.dt.year  # 提取日期时间属性
# 删除日期、星期、年份等非特征列
df.drop(['Date','weekday','year'], axis=1, inplace=True)
target = 'Weekly_Sales'  # 指定目标变量为"Weekly_Sales"
 
# 相关性分析和特征选择
plt.figure(figsize=(10,8))
# 绘制变量两两关系图
sns.pairplot(df, x_vars=['Holiday_Flag', 'Temperature', 'Fuel_Price', 'CPI', 'Unemployment','month'],
            y_vars=['Weekly_Sales'], height=4, aspect=0.7, kind='reg')  # 定义列表y_vars
plt.savefig("1.png")  # 保存图形至文件
plt.show()  # 显示图形
 
df1=df.copy(deep=True)  # 深拷贝数据框用于独立分析(不影响原始数据)
df1.drop(['Store'], axis=1, inplace=True)  # 删除不需要的列'Store'
corr_matrix = df1.corr()  # 计算相关系数矩阵
plt.figure(figsize=(10,8))  # 创建图形画布
sns.heatmap(corr_matrix, annot=True, cmap='Reds')  # 绘制热力图
plt.savefig("2.png")  # 保存图形至文件
plt.show()  # 显示图形
 
df.drop(['Fuel_Price'], axis=1, inplace=True)  # 删除不需要的列'Fuel_Price'
# 分类数据处理
df = pd.get_dummies(df, columns=['Store','month'])
 
# 提取特征和标签
X = df.drop([target],axis=1)
y = df[target]  # 提取目标变量列赋值给y
 
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=100)

# 数据标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)  # 对数据进行变换
X_test = scaler.transform(X_test)  # 对数据进行变换
 
# 训练多元线性回归模型
model = LinearRegression()
model.fit(X_train, y_train)  # 在数据上训练model模型
 
# 测试集预测
y_pred = model.predict(X_test)
r2 =  r2_score(y_test, y_pred)  # 计算R²评估模型拟合优度
# 评估模型预测效果
print('R平方值(R^2):', r2)

预期输出:数据文件平台内置,本地无数据,具体数值以平台运行结果为准。判读要点:

  1. 文本输出:仅最后一行 R平方值(R^2): ... 的一个数值。判读时先看符号与量级:接近1说明测试集拟合好;明显偏低甚至为负,说明线性假设或特征集还有改进空间。
  2. 图形输出(1.png):Holiday_Flag、Temperature、Fuel_Price、CPI、Unemployment、month 六个特征对 Weekly_Sales 的带回归线散点图。判读要点:各子图回归线斜率是否明显、点云是否松散;斜率接近0且点云松散的特征,预测能力弱。
  3. 图形输出(2.png):剔除 Store 列后数值变量的相关系数热力图(Reds顺序配色,颜色越深正相关越强,负相关与无相关均呈浅色)。判读要点:各特征与 Weekly_Sales 所在行列的颜色深浅,以及特征彼此之间是否高度相关(多重共线性线索)。

22.5 特征工程

列表 22.2: 特征工程与相关性分析
# 注:本块是平台任务的分步讲解版(重述特征工程与相关性分析部分)。若已在平台运行过完整任务,请勿紧接着重复运行本块(Fuel_Price 列已被删除,会报 KeyError);建议在平台新会话中单独按块顺序运行讲解块

# =============================================================================
# 题目:特征工程与特征相关性分析
# =============================================================================
# 本代码进行特征工程,包括相关性分析和特征选择。通过可视化各特征
# 与目标变量的关系,识别重要特征并剔除冗余特征,提升模型性能和
# 可解释性。这是机器学习项目中至关重要的一步。

# ==================== 定义特征列表 ====================
# 选择可能影响销售额的特征变量
features = ['Holiday_Flag', 'Temperature', 'Fuel_Price', 'CPI', 'Unemployment', 'month']
# Holiday_Flag: 假期标志,假期销售通常更高
# Temperature: 温度,可能影响购物意愿
# Fuel_Price: 燃油价格,影响消费者的出行和购买力
# CPI: 消费价格指数,衡量通胀水平
# Unemployment: 失业率,反映经济状况
# month: 月份,捕捉季节性规律

# 定义目标变量(要预测的变量)
target = 'Weekly_Sales'  # 周销售额是本项目的预测目标

# ==================== 绘制散点图矩阵 ====================
# 使用Seaborn绘制散点图矩阵,展示各特征与目标变量的关系
sns.pairplot(
    df,  # 数据源DataFrame
    x_vars=features,  # x轴变量列表(所有特征)
    y_vars=[target],  # y轴变量列表(目标变量)
    height=4,  # 每个子图的高度(英寸)
    aspect=0.7,  # 子图的宽高比(宽度/高度)
    kind='reg'  # 绘制带回归线的散点图
)
plt.suptitle('各特征与销售额的关系', y=1.02)  # 设置总标题,y=1.02将标题向上移动避免重叠
plt.show()  # 显示散点图矩阵

# ==================== 计算相关性矩阵 ====================
# 创建数据副本,避免修改原始数据
df1 = df.copy(deep=True)  # deep=True创建深拷贝,包括数据和索引

# 删除Store列(分类变量,无法计算相关系数)
df1.drop(['Store'], axis=1, inplace=True)  # axis=1表示删除列,inplace=True直接在原数据上修改

# 计算所有数值变量的相关系数矩阵
corr_matrix = df1.corr()  # corr()方法计算皮尔逊相关系数,返回对称矩阵
# 相关系数范围[-1, 1],绝对值越大相关性越强

# ==================== 绘制相关性热力图 ====================
plt.figure(figsize=(10, 8))  # 创建画布,尺寸为10x8英寸

# 使用Seaborn绘制热力图,可视化相关系数矩阵
sns.heatmap(corr_matrix, annot=True, cmap='Reds', center=0)
# annot=True在每个格子中显示相关系数的数值
# cmap='Reds'使用红色系配色方案(颜色越深相关性越强)
# center=0将颜色映射的中心设为0(负值与0落在色标浅端,0的具体深浅取决于最强负相关的位置,正值越强越深)

plt.title('特征相关性热力图')  # 设置图表标题
plt.show()  # 显示热力图

# ==================== 特征选择 ====================
# 根据相关性分析结果,删除与目标变量相关性低的特征
df.drop(['Fuel_Price'], axis=1, inplace=True)  # 删除燃油价格列
# 如果Fuel_Price与Weekly_Sales的相关系数接近0,说明它对预测没有帮助
# 删除不相关特征可以降低模型复杂度,避免过拟合

# ==================== 输出解读 ====================
# 散点图矩阵展示了各特征与销售额的线性关系:
# - 如果点的分布近似直线,说明存在线性关系
# - 如果回归线斜率较大,说明该特征对销售额影响显著
# - 如果点很分散,说明该特征预测能力弱
#
# 热力图展示了所有特征之间的相关性:
# - 本图使用Reds顺序配色,颜色越深表示正相关越强
# - 负相关与无相关均呈浅色(接近白色),二者无法直接区分
# - 若需同时直观展示正负相关,应改用RdBu等双色渐变(diverging)配色
#
# 特征选择原则:
# 1. 删除与目标变量相关性低的特征
# 2. 删除与其他特征高度相关的特征(多重共线性)
# 3. 保留与目标变量相关性高的特征

22.6 模型构建

上面特征工程讲解块覆盖了平台流水线的前半段(特征构造与筛选);后半段的”编码→划分→标准化→训练→评估”不再逐行重述,改为把其中最容易写错的一步单独拎出来做常见错误演示:把平台代码中的 X_test = scaler.transform(X_test) 换成初学者常写的 X_test = scaler.fit_transform(X_test),其余环节保持不变。演示在一份可本地运行的合成数据上进行(150行”单店周度”记录,列名与平台数据一致),并刻意给 train_test_split 加上 shuffle=False 以保留时间顺序,模拟”冬春季历史周训练、夏季未来周考试”的真实预测场景。请重点观察三个数字:考试期(测试集)的温度均值、两种写法各自的测试集R²、以及测试集温度被标准化后的均值。

列表 22.3: 常见错误演示:测试集误用 fit_transform
# 本地演练说明:平台内置的01_Walmart_Store_sales.csv本地没有,以下用同结构的内联合成数据演示标准化泄漏
import numpy as np  # 导入NumPy数值计算库
import pandas as pd  # 导入Pandas数据分析库
from sklearn.preprocessing import StandardScaler  # 导入标准化器
from sklearn.model_selection import train_test_split  # 导入训练集测试集划分函数
from sklearn.linear_model import LinearRegression  # 导入线性回归模型
from sklearn.metrics import r2_score  # 导入R²评估函数

rng = np.random.default_rng(100)  # 固定随机种子,与平台代码的random_state=100呼应
temperature = np.concatenate([rng.normal(5, 5, 120), rng.normal(30, 8, 30)])  # 前120行为冬春季周记录、后30行进入夏季,温度整体抬升
unemployment = rng.normal(8, 2, 150)  # 失业率特征,均值8、标准差2
weekly_sales = 1000 + 15*temperature - 40*unemployment + rng.normal(0, 20, 150)  # 线性数据生成机制:温度推高销售、失业率压制销售
X = pd.DataFrame({'Temperature': temperature, 'Unemployment': unemployment})  # 特征矩阵,列名同平台数据
y = pd.Series(weekly_sales, name='Weekly_Sales')  # 目标变量,列名同平台数据
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, shuffle=False)  # 按时间顺序8:2切分:历史周训练、未来周考试

scaler = StandardScaler()  # 正确做法:标准化器只在训练集上学习均值和标准差
X_train_s = scaler.fit_transform(X_train)  # 训练集执行fit_transform
X_test_s = scaler.transform(X_test)  # 测试集只执行transform,沿用训练集的统计量
r2_correct = r2_score(y_test, LinearRegression().fit(X_train_s, y_train).predict(X_test_s))  # 正确流程的测试集R²

leaky = StandardScaler()  # 错误做法:给测试集另配一个标准化器
X_train_l = leaky.fit_transform(X_train)  # 训练集的处理与上面完全相同
X_test_l = leaky.fit_transform(X_test)  # 常见错误:测试集误用fit_transform,改用测试集自己的均值和标准差
r2_leaky = r2_score(y_test, LinearRegression().fit(X_train_l, y_train).predict(X_test_l))  # 泄漏流程的测试集R²

print('训练期/考试期温度均值: %.2f / %.2f' % (X_train['Temperature'].mean(), X_test['Temperature'].mean()))  # 两段样本的温度水平差异
print('正确做法(测试集只transform)的测试集R²: %.4f' % r2_correct)  # 正确流程的评估结果
print('常见错误(测试集fit_transform)的测试集R²: %.4f' % r2_leaky)  # 泄漏流程的评估结果
print('测试集Temperature标准化后的均值: 正确 %.4f / 泄漏 %.4f' % (X_test_s[:, 0].mean(), X_test_l[:, 0].mean()))  # 泄漏把考试期数据"重新居中"

预期输出(本机实际运行结果):

训练期/考试期温度均值: 5.42 / 31.42
正确做法(测试集只transform)的测试集R²: 0.9790
常见错误(测试集fit_transform)的测试集R²: -7.3393
测试集Temperature标准化后的均值: 正确 5.4645 / 泄漏 -0.0000

两种写法只差一个 fit,评估结论却天差地别:正确做法的测试集R²约为0.98,误用 fit_transform 后跌到约-7.34。原因在最后一行输出:考试期的温度均值是31.42℃,按训练期口径标准化后其均值约为+5.46,模型”知道”这是一段比历史更热的时期;泄漏写法却用测试集自己的均值和标准差重新居中,均值被压回0,模型看到的考试样本仿佛回到了”平均温度的历史周”,夏季带来的销售额抬升被整体抹掉,预测因此严重偏低。两点提醒:其一,泄漏的方向并不固定——本例表现为大幅偏低,换一种数据形态也可能表现为虚高,所以它的性质不是”乐观偏差”,而是”评估失效”;其二,凡是带 fit 的预处理都只能在训练集上拟合,测试集一律只 transform,这是流水线中不可妥协的纪律。回到平台任务本身,请保持平台原样代码中的 scaler.transform(X_test) 不变。

22.7 分析结论

本章流水线完成的事情可以概括为:把一张含日期、商店编号与若干外部变量的周度销售表,整理成”数值特征矩阵 + 销售额标签”的标准建模形态,再以多元线性回归在8:2划分下完成训练与测试,并用测试集R²作为唯一的汇报指标。本章数值结论以平台运行结果为准(本地无数据),以下仅做定性判读。

特征工程方面,流水线做了三件事:一是从 Date 派生星期、月份、年份,并按业务常识删除日期与年份列,只保留月份以捕捉季节性;二是用散点图矩阵与热力图做相关性检查,据此删除与目标相关性弱的 Fuel_Price;三是把 Storemonth 两个类别变量独热编码,使其能够进入线性模型——商店间的固有差异往往是这类数据中最重要的解释来源之一。

R² 的解读要克制:它只度量测试集上预测值与真实值的总体贴近程度,既不保证每个商店、每个时段都预测得准,也不说明因果关系。若平台结果显示R²较高,合理解释是商店个体差异与月份季节性被虚拟变量捕捉到了;若偏低,应先怀疑线性假设或遗漏交互项,而不是直接断言”数据没有规律”。该模型可用于备货量级层面的粗估;用于精细到单店单周的排班与促销决策之前,还应补充残差分析并经业务复核。

22.8 本章小结

本章新增的技能要点:

  1. 流水线意识:读数、特征构造、特征筛选、编码、划分、标准化、训练、评估八步顺序不可颠倒,尤其编码与划分的先后、标准化与划分的先后各有讲究。
  2. 日期特征化:pd.to_datetime 之后用 .dt.weekday.dt.month.dt.year 把日期变成可建模的数值特征。
  3. 特征筛选:sns.pairplot(kind='reg') 看单特征线性趋势,df.corr() 加热力图看两两相关,双图互补决定特征去留。
  4. 独热编码:pd.get_dummies 处理类别变量;删除 Fuel_Price 这类弱相关特征可简化模型、降低过拟合风险。
  5. 防泄漏习惯:fit_transform 只作用于训练集,测试集只 transform;random_state 固定保证结果可复现。

易错点:

  • 测试集误写 X_test = scaler.fit_transform(X_test):测试集被自己的均值和标准差重新居中,评估随之失效,正文本地演练中 R² 从约0.98跌到约-7.34
  • 对全部数据先标准化再划分:统计量混入测试集信息,与上一条同属泄漏;正确顺序是先 train_test_split,再在训练集上 fit_transform
  • 以为 pd.read_csv(..., parse_dates=True) 已把 Date 列转为日期:不指定列名时它并不转换,必须再执行 pd.to_datetime(df.Date) 才能用 .dt.weekday 等提取特征
  • 把 R² 当 0~1 的”准确率”读:它度量的是与均值基准的差距,可以为负,负值意味着比直接猜均值更差
  • 算相关系数时把 Store 编号留在数值列里:商店编号是ID型类别,相关系数没有意义,应先删除再 corr();且 cmap='Reds' 下负相关与无相关同为浅色,浅色格不能一律读成”不相关”
  • 平台任务跑完后紧接着重跑特征工程讲解块会报 KeyError:Fuel_Price 列已被删除,讲解块应在平台新会话中单独运行

22.9 动手与思考

以下练习每题附参考答案(默认折叠);概念题与变式题可对照自查,商业判断题不设唯一答案,判断依据与口径比结论更重要。

  1. 概念辨析:训练集R²与测试集R²有何区别?为什么本章只汇报测试集R²,而且它有可能为负?

    参考答案(点开前请先独立完成)

    解题思路:训练集R²是把模型“学习过”的样本既当课本又当考卷算出来的分数,衡量的是拟合能力;测试集R²用的全是模型没见过的样本,衡量的是泛化能力。本章只汇报测试集R²,是因为模型的最终用途是预测没见过的未来周——训练集R²再高也只说明“背书”背得好,不能证明对新样本有用。它有可能为负,是因为R²的定义是与“直接用测试集均值来猜”的基准比较:当模型的残差平方和超过该基准的总离差平方和时,R²就小于零,含义是比无脑猜均值更差(本章“模型构建”一节的常见错误演示中,泄漏流程的测试集R²约为-7.34,正是这种情形)。另需说明:在简单线性模型加干净数据的情形下两个R²可能非常接近;模型越复杂、样本越少、噪声越大,训练集R²越容易明显高于测试集R²,两者之差是过拟合的常用信号。

    回扣本章:对应“本章新增的技能要点”第1条(划分、标准化、训练、评估的先后顺序不可颠倒)与第5条(random_state 固定保证结果可复现)。

  2. 概念辨析:scaler.fit_transform(X_train)scaler.transform(X_test) 的差别是什么?如果对测试集单独 fit,会造成什么问题?

    参考答案(点开前请先独立完成)

    解题思路fit_transform 等于先 fit(在传入的数据上计算均值与标准差)再 transform(按这一套统计量做缩放);transform 只做缩放,统计量沿用标准化器上次 fit 的结果。平台任务(列表 22.1)在训练集上 fit_transform、测试集上只 transform,含义是考试期的数据必须按历史期的均值和标准差来缩放。若对测试集单独 fit(典型错误写法 X_test = scaler.fit_transform(X_test)),测试集会被它自己的均值和标准差重新居中、缩放,相当于让考卷迁就考生——评估不再度量真实泛化能力,且偏差方向并不固定:可能虚高,也可能像正文本地演练那样,考试期偏高的温度被压回均值0、夏季带来的销售额抬升被整体抹掉,R²从约0.98跌到约-7.34。结论是:凡带 fit 的预处理只能在训练集上做,这是评估可信的前提。

    回扣本章:对应“本章新增的技能要点”第5条(fit_transform 只作用于训练集,测试集只 transform)。

  3. 变式任务:把评估指标从R²换成MAE或RMSE(从 sklearn.metrics 导入相应函数),改造思路是什么?这两个指标在业务上分别如何解读(平均每期偏差多少金额)?

    参考答案(点开前请先独立完成)

    改造思路:流水线前段(读数、日期特征构造、特征筛选、独热编码、划分、标准化、训练、预测)一行不动,只替换评估环节的三处:导入语句由 r2_score 换成 mean_absolute_errormean_squared_error;指标计算两行分别求 MAE 与 RMSE;print 输出行同步替换。业务解读:MAE 把每期预测偏差取绝对值后平均,单位与 Weekly_Sales 相同,可直接读成“平均每期偏差多少金额”;RMSE 先把偏差平方、取平均再开方,同样以金额为单位,但对大偏差惩罚更重。两者结合可判断误差形态:RMSE 明显大于 MAE,说明误差集中在少数期;两者接近,说明误差均匀分布在各期。

    # 变式代码:只替换评估环节(读数、特征构造、独热编码与@lst-ch22-platform-task完全一致,此处不重复)
    from sklearn.metrics import mean_absolute_error, mean_squared_error  # ← 改动点1:导入MAE与均方误差函数(可保留r2_score作对照)
    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=100)  # 与平台任务一致
    scaler = StandardScaler()  # 与平台任务一致
    X_train = scaler.fit_transform(X_train)  # 与平台任务一致:训练集拟合并变换
    X_test = scaler.transform(X_test)  # 与平台任务一致:测试集只变换(防泄漏纪律不变)
    model = LinearRegression()  # 与平台任务一致
    model.fit(X_train, y_train)  # 与平台任务一致
    y_pred = model.predict(X_test)  # 与平台任务一致:测试集预测
    mae = mean_absolute_error(y_test, y_pred)  # ← 改动点2:MAE即逐期偏差取绝对值后平均,单位为金额
    rmse = mean_squared_error(y_test, y_pred) ** 0.5  # ← 改动点2:RMSE即偏差平方取平均后开方,对大误差惩罚更重
    print('测试集MAE(平均每期偏差金额):', mae)  # ← 改动点3:输出行同步替换
    print('测试集RMSE(放大大误差的口径):', rmse)  # ← 改动点3:输出行同步替换

    结构性判读:文本输出由一行 R平方值(R^2): … 变为两行金额口径的误差值,两幅图形输出完全不变(特征工程部分未改动)。判读要点:先看MAE的量级是否在业务可接受范围内(备货量级允许的每期偏差),再比较RMSE与MAE的相对大小——RMSE远大于MAE说明个别周预测严重失准,应回查异常周;接近则说明误差普遍而温和。

    本地演练版(模拟数据):在与本章“模型构建”一节同型的150行合成数据上实跑(列名同平台数据,random_state=100):

    # 本地演练版:在与正文本地演练同型的合成数据上对比MAE与RMSE两种误差口径
    import numpy as np  # 导入NumPy数值计算库
    import pandas as pd  # 导入Pandas数据分析库
    from sklearn.model_selection import train_test_split  # 导入训练集测试集划分函数
    from sklearn.linear_model import LinearRegression  # 导入线性回归模型
    from sklearn.metrics import mean_absolute_error, mean_squared_error  # 导入MAE与均方误差评估函数
    rng = np.random.default_rng(100)  # 固定随机种子,与平台代码的random_state=100呼应
    temperature = np.concatenate([rng.normal(5, 5, 120), rng.normal(30, 8, 30)])  # 前120行冬春季记录、后30行进入夏季
    unemployment = rng.normal(8, 2, 150)  # 失业率特征,均值8、标准差2
    weekly_sales = 1000 + 15 * temperature - 40 * unemployment + rng.normal(0, 20, 150)  # 线性数据生成机制
    X = pd.DataFrame({'Temperature': temperature, 'Unemployment': unemployment})  # 特征矩阵,列名同平台数据
    y = pd.Series(weekly_sales, name='Weekly_Sales')  # 目标变量,列名同平台数据
    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=100)  # 8:2随机划分
    model = LinearRegression().fit(X_train, y_train)  # 训练多元线性回归模型
    y_pred = model.predict(X_test)  # 测试集预测
    print('MAE(平均每期偏差金额): %.2f 元' % mean_absolute_error(y_test, y_pred))  # 平均绝对误差:逐期偏差取绝对值后平均
    print('RMSE(均方根误差): %.2f 元' % mean_squared_error(y_test, y_pred) ** 0.5)  # 均方根误差:先平方再平均后开方,放大大误差
    print('测试集周数:', len(y_test))  # 测试集样本量

    模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):

    MAE(平均每期偏差金额): 15.63 元
    RMSE(均方根误差): 18.31 元
    测试集周数: 30

    演练判读:这份干净合成数据上RMSE(18.31)仅略高于MAE(15.63),误差接近均匀分布;若平台数据上RMSE远大于MAE,应优先排查个别异常周(节假日、促销脉冲)。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 22.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第1条(评估是八步流水线的末环,换指标只动末环,不动前段)。

  4. 变式任务:若把 train_size 改为0.7、random_state 改为其他数值,流水线中哪些环节完全不受影响、哪些输出会随之改变?请说明理由,再到平台上验证。

    参考答案(点开前请先独立完成)

    改造思路:唯一改动在 train_test_split 一行——train_size=0.8, test_size=0.2, random_state=100 换成 train_size=0.7, test_size=0.3, random_state=42(比例之和仍须为1)。完全不受影响的环节:读取数据、pd.to_datetime.dt 特征构造、散点图矩阵与相关系数热力图、剔除 Fuel_Priceget_dummies 独热编码、X 与 y 的生成——这些只依赖数据本身,与样本怎么切分无关。随之改变的输出:训练集与测试集的行数、StandardScaler 学到的均值与标准差、回归系数,以及最终R²数值。理由:划分参数决定多少样本进入“课本”,随机种子决定具体抽到哪些行;标准化统计量与模型都在新的训练集上重新估计,但流水线结构不变。

    # 变式代码:切换train_size与random_state(两参数所在行即唯一改动,其余环节与平台任务一致)
    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, test_size=0.3, random_state=42)  # ← 改动点:0.8/0.2/100改为0.7/0.3/42
    scaler = StandardScaler()  # 与平台任务一致
    X_train = scaler.fit_transform(X_train)  # 与平台任务一致:统计量随新训练集重算
    X_test = scaler.transform(X_test)  # 与平台任务一致:测试集只变换
    model = LinearRegression().fit(X_train, y_train)  # 与平台任务一致:模型在新训练集上重估
    y_pred = model.predict(X_test)  # 与平台任务一致
    r2 = r2_score(y_test, y_pred)  # 与平台任务一致
    print('R平方值(R^2):', r2)  # 与平台任务一致:数值会随划分参数改变

    结构性判读:文本输出仍是最后一行的单个R²,数值会变但一般仍处同一量级;两幅图形输出与原任务完全一致。判读要点:对比改参前后的R²变化幅度——小幅波动说明结论对划分方式稳健;大幅波动说明样本量偏小或结论不稳,应报告多种划分下的结果区间而不是单点数值。

    本地演练版(模拟数据):在同一份合成数据上切换三组划分参数(列名同平台数据,random_state=100):

    # 本地演练版:同一合成数据上切换train_size与random_state,观察哪些输出随之改变
    import numpy as np  # 导入NumPy数值计算库
    import pandas as pd  # 导入Pandas数据分析库
    from sklearn.model_selection import train_test_split  # 导入训练集测试集划分函数
    from sklearn.linear_model import LinearRegression  # 导入线性回归模型
    from sklearn.preprocessing import StandardScaler  # 导入标准化器
    from sklearn.metrics import r2_score  # 导入R²评估函数
    rng = np.random.default_rng(100)  # 固定数据生成种子,三组实验共用同一份数据
    temperature = rng.normal(20, 10, 150)  # 温度特征
    unemployment = rng.normal(8, 2, 150)  # 失业率特征
    weekly_sales = 1000 + 15 * temperature - 40 * unemployment + rng.normal(0, 20, 150)  # 线性数据生成机制
    X = pd.DataFrame({'Temperature': temperature, 'Unemployment': unemployment})  # 特征矩阵,列名同平台数据
    y = pd.Series(weekly_sales, name='Weekly_Sales')  # 目标变量,列名同平台数据
    print('特征工程输出与划分参数无关:温度列均值 %.4f' % X['Temperature'].mean())  # 划分之前的一切输出只依赖数据本身
    for train_size, seed in [(0.8, 100), (0.7, 100), (0.7, 42)]:  # 三组划分参数:先改比例,再换种子
        X_tr, X_te, y_tr, y_te = train_test_split(X, y, train_size=train_size, random_state=seed)  # 改动点:两个参数在此切换
        scaler = StandardScaler()  # 标准化器在新的训练集上重新拟合
        X_tr_s = scaler.fit_transform(X_tr)  # 训练集拟合并变换
        model = LinearRegression().fit(X_tr_s, y_tr)  # 在标准化后的训练集上训练
        r2 = r2_score(y_te, model.predict(scaler.transform(X_te)))  # 测试集沿用训练集统计量做变换后评估
        print('train_size=%.1f, random_state=%d → 训练集%d行,测试集R² %.4f' % (train_size, seed, len(X_tr), r2))  # 随参数变化的输出

    模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):

    特征工程输出与划分参数无关:温度列均值 21.0266
    train_size=0.8, random_state=100 → 训练集120行,测试集R² 0.9881
    train_size=0.7, random_state=100 → 训练集105行,测试集R² 0.9899
    train_size=0.7, random_state=42 → 训练集105行,测试集R² 0.9838

    演练判读:三组参数下训练集行数由120变为105、R²在0.98上下小幅波动,而特征工程的输出(温度列均值)与划分参数无关——与上面的推理一致。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 22.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第5条(random_state 固定保证可复现;切换参数检验结论稳健性是同一纪律的延伸)。

  5. 商业判断:若平台运行结果显示R²仅略高于0.5,而门店经理希望据此直接制定下周的排班与促销预算,你会给出什么经营建议?请从”模型可用边界”与”还需补充的信息”两方面作答。

    参考答案(点开前请先独立完成)

    参考作答框架:本题不设唯一结论,优秀作答应覆盖以下维度。

    • 口径先行:先确认这个R²的口径——测试集、8:2划分、random_state=100 下的单次结果(列表 22.1);第4题的演练说明换一个随机种子数值就会波动,因此“略高于0.5”应作为区间读数而非精确分数;R²也不区分高估与低估,反映不出季节性与节假日等误差结构。
    • 模型可用边界:R²约0.5只支持量级层面的参考(如总量备货粗估),不支持直接落到单店单周的排班与促销预算;使用前至少应按商店、按月份分解残差,确认误差没有集中在个别门店或时段,并做滚动回测(近几周“预测对实际”)。
    • 常见错误作答形态:一是“R²不到0.6就是坏模型”或“R²超过0.5就够用”两个极端,把单一指标当充分条件;二是把R²误当预测准确率;三是不交代划分方式与口径就下结论。
    • 还需补充的信息:促销与假期日历、天气、更长历史(覆盖更多季节周期)、分店误差分布与预测区间、业务可接受的金额误差容限(用第3题的MAE或RMSE口径表述)。
    • 建议表述方向:先把它定位为“总量级参考+人工复核”的辅助工具,同步开展误差分解与回测,达标后再逐步用于预算决策。

    回扣本章:对应“本章新增的技能要点”第1条(评估结论必须连同口径一起汇报)。